eletrotupi / tcc/ frontend/app/entry/[id].tsxde5e5eca8460913bbc90890ce76ac0b905dae0cd
8.7 KB | 317 lines Raw
1
import { useState, useEffect } from "react";
2
import {
3
  StyleSheet,
4
  Text,
5
  View,
6
  ScrollView,
7
  Pressable,
8
  ActivityIndicator,
9
} from "react-native";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { Badge } from "@/components/ui/Badge";
16
import { Button } from "@/components/ui/Button";
17
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
18
import { Section, SectionHeader } from "@/components/ui/Sections";
19
import { Theme, Colors, Spacing } from "@/constants/theme";
20
import { getMood } from "@/constants/moods";
21
import {
22
  getMoodComponent,
23
  intensityLabel,
24
  intensityToValue,
25
} from "@/constants/mood-components";
26
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
27
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
28
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
29

30
export default function MoodDetailScreen() {
31
  const { id } = useLocalSearchParams<{ id: string }>();
32
  const { data: entry, isLoading, isError } = useMoodEntry(id);
33
  const deleteMoodEntry = useDeleteMoodEntry();
34
  const [minutesLeft, setMinutesLeft] = useState(0);
35
  const [canDelete, setCanDelete] = useState(false);
36

37
  useEffect(() => {
38
    const updateTimer = () => {
39
      const moment = new Date(entry?.moment);
40
      const createdAt = new Date(entry?.moment);
41
      const msSinceCreation = Date.now() - createdAt.getTime();
42
      const deletionWindowMs = 5 * 60 * 1000;
43

44
      const newCanDelete = msSinceCreation < deletionWindowMs;
45
      const newMinutesLeft = Math.max(
46
        0,
47
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
48
      );
49

50
      setCanDelete(newCanDelete);
51
      setMinutesLeft(newMinutesLeft);
52
    };
53

54
    updateTimer(); // Set initial values
55

56
    const interval = setInterval(updateTimer, 1000);
57

58
    return () => clearInterval(interval);
59
  }, [entry?.moment]);
60

61
  if (isLoading) {
62
    return (
63
      <View style={styles.centered}>
64
        <ActivityIndicator size="large" color={Theme.colors.tint} />
65
      </View>
66
    );
67
  }
68

69
  if (isError || !entry) {
70
    return (
71
      <View style={styles.centered}>
72
        <Text style={styles.errorText}>Registro não encontrado.</Text>
73
        <Pressable onPress={() => router.back()} style={styles.backButton}>
74
          <Text style={styles.backButtonText}>Voltar</Text>
75
        </Pressable>
76
      </View>
77
    );
78
  }
79

80
  const moment = new Date(entry.moment);
81
  const moodDef = getMood(entry.selectedMood.toLowerCase());
82
  const badge = getTimeBadge(moment);
83

84
  const handleDelete = async () => {
85
    await deleteMoodEntry.mutateAsync(String(entry.id));
86

87
    router.back();
88
  };
89

90
  const deleteButtonMsg = () => {
91
    const msg = "Excluir Registro";
92
    const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
93
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
94

95
    return `${msg} ${suffix}`;
96
  };
97

98
  return (
99
    <>
100
      <Stack.Screen options={{ title: "Ver detalhes" }} />
101

102
      <ScrollView
103
        style={styles.container}
104
        contentContainerStyle={styles.content}
105
        showsVerticalScrollIndicator={false}
106
      >
107
        {/* Header card */}
108
        <Card style={styles.headerCard}>
109
          <Between>
110
            <Row gap={12} style={{ alignItems: "center" }}>
111
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
112
              <Col gap={2}>
113
                <Text style={styles.moodLabel}>
114
                  {moodDef?.label ?? entry.selectedMood}
115
                </Text>
116
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
117
              </Col>
118
            </Row>
119
            <Badge
120
              label="Registro Ativo"
121
              backgroundColor={Theme.colors.activeBackground}
122
              color={Theme.colors.tint}
123
            />
124
          </Between>
125
        </Card>
126

127
        {/* Core levels */}
128
        <Section>
129
          <SectionHeader title="Níveis emocionais" variant="subtle" />
130
          {[
131
            { id: "energy", label: "Energia", value: entry.energyLevel },
132
            { id: "stress", label: "Estresse", value: entry.stressLevel },
133
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
134
          ].map((item, index, arr) => (
135
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
136
              <View key={item.label}>
137
                <View style={styles.componentRow}>
138
                  <Text style={styles.componentLabel}>{item.label}</Text>
139
                  <Text style={styles.componentIntensityGreen}>
140
                    {item.value}/10
141
                  </Text>
142
                </View>
143

144
                <View style={styles.progressTrack}>
145
                  <View
146
                    style={[
147
                      styles.progressFill,
148
                      { width: `${(item.value / 10) * 100}%` },
149
                    ]}
150
                  />
151
                </View>
152
              </View>
153
            </Card>
154
          ))}
155
        </Section>
156

157
        <Section>
158
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
159
          <Col gap={0}>
160
            {entry.moodComponents.map((comp, index) => {
161
              const def = getMoodComponent(comp.component.toLowerCase());
162
              const numericIntensity =
163
                comp.intensity === "LIGHT"
164
                  ? 2
165
                  : comp.intensity === "MODERATE"
166
                    ? 5
167
                    : 8;
168

169
              return (
170
                <MoodComponentCard
171
                  key={comp.id}
172
                  id={comp.component.toLowerCase()}
173
                  intensity={comp.intensity}
174
                />
175
              );
176
            })}
177
          </Col>
178
        </Section>
179

180
        {/* Annotation / notes */}
181
        {entry.annotation && (
182
          <Section>
183
            <SectionHeader title="Anotações" variant="subtle" />
184
            <Card style={{ padding: Spacing.cardGap }}>
185
              <Text style={styles.annotation}>
186
                "{entry.annotation?.trim()}"
187
              </Text>
188
            </Card>
189
          </Section>
190
        )}
191

192
        {/* Delete */}
193
        <View style={styles.section}>
194
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
195
          <Button
196
            variant="danger"
197
            isLoading={deleteMoodEntry.isPending}
198
            disabled={!canDelete || deleteMoodEntry.isPending}
199
            onPress={handleDelete}
200
            title={deleteButtonMsg()}
201
          />
202

203
          <SubtleInfoCard
204
            style={styles.deleteInfo}
205
            text={`
206
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
207
              após o envio para garantir a integridade do histórico emocional.
208
              `}
209
          />
210
        </View>
211
      </ScrollView>
212
    </>
213
  );
214
}
215

216
const styles = StyleSheet.create({
217
  container: {
218
    flex: 1,
219
    backgroundColor: Theme.colors.background,
220
  },
221
  content: {
222
    padding: Spacing.containerPadding,
223
    gap: Spacing.sectionGap,
224
    paddingBottom: 48,
225
  },
226
  centered: {
227
    flex: 1,
228
    justifyContent: "center",
229
    alignItems: "center",
230
    gap: 12,
231
  },
232
  errorText: {
233
    fontSize: 15,
234
    color: Colors.light.textSecondary,
235
  },
236
  backButton: {
237
    paddingHorizontal: 20,
238
    paddingVertical: 10,
239
    backgroundColor: Theme.colors.tint,
240
    borderRadius: Theme.borderRadius.md,
241
  },
242
  backButtonText: {
243
    fontWeight: "700",
244
    color: Theme.colors.text,
245
  },
246

247
  // Header
248
  headerCard: {
249
    padding: 16,
250
  },
251
  moodIcon: {
252
    fontSize: 40,
253
  },
254
  moodLabel: {
255
    fontSize: 17,
256
    fontWeight: "700",
257
    color: Colors.light.text,
258
  },
259
  moodTime: {
260
    fontSize: 13,
261
    color: Colors.light.textSecondary,
262
  },
263

264
  // Sections
265
  section: {
266
    gap: 8,
267
  },
268
  sectionLabel: {
269
    fontSize: 11,
270
    fontWeight: "600",
271
    letterSpacing: 0.8,
272
    color: Colors.light.textSecondary,
273
    textTransform: "uppercase",
274
  },
275

276
  // Components
277
  componentRow: {
278
    flexDirection: "row",
279
    justifyContent: "space-between",
280
    alignItems: "center",
281
    paddingVertical: 10,
282
  },
283
  componentLabel: {
284
    fontSize: 15,
285
    fontWeight: "700",
286
    color: Colors.light.text,
287
  },
288
  componentIntensity: {
289
    fontSize: 13,
290
    color: Colors.light.textSecondary,
291
  },
292
  componentIntensityGreen: {
293
    fontSize: 13,
294
    fontWeight: "700",
295
    color: Theme.colors.tint,
296
  },
297
  progressTrack: {
298
    height: 6,
299
    backgroundColor: Theme.colors.light.divider,
300
    borderRadius: Theme.borderRadius.full,
301
    marginBottom: 4,
302
    overflow: "hidden",
303
  },
304
  progressFill: {
305
    height: "100%",
306
    backgroundColor: Theme.colors.light.tint,
307
    borderRadius: Theme.borderRadius.full,
308
  },
309
  // Annotation
310
  annotation: {
311
    fontSize: 14,
312
    lineHeight: 22,
313
    color: Colors.light.textSecondary,
314
    fontStyle: "italic",
315
    padding: 4,
316
  },
317
});